﻿using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Microsoft.Net.Http.Headers;
using PMS.Core.Configuration;
using PMS.Core.Infrastructure;
using PMS.Data.Entities.Settings;
using PMS.Services.EntityServices.Localization;
using PMS.Services.EntityServices.Logging;
using PMS.Services.Middlewares;
using PMS.Services.Plugins;
using PMS.Services.SharedKernel;
using PMS.Services.Utils;
using PMS.Services.Utils.ScheduleTasks;
using PMS.Web.Framework.Mvc.Routing;
using PMS.Web.Framework.StartupCfgs.StartupSetups;
using System;
using System.Globalization;
using System.Linq;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using WebMarkupMin.AspNetCore3;

namespace PMS.Web.Framework.StartupCfgs
{
    /// <summary>
    /// Represents extensions of IApplicationBuilder
    /// </summary>
    public static class ApplicationBuilderExtensions
    {
        /// <summary>
        /// Configure the application HTTP request pipeline
        /// </summary>
        /// <param name="application">Builder for configuring an application's request pipeline</param>
        public static void ConfigureRequestPipeline(this IApplicationBuilder application)
        {
            EngineContext.Current.ConfigureRequestPipeline(application);
        }

        public static void StartEngine(this IApplicationBuilder application)
        {
            var engine = EngineContext.Current;

            //initialize and start schedule tasks
            TaskManager.Instance.Initialize();

            //log application start
            engine.Resolve<ILogger>().Debug("Application started");

            var pluginService = engine.Resolve<IPluginService>();

            //install plugins
            pluginService.InstallPlugins();

            //update plugins
            pluginService.UpdatePlugins();
        }

        /// <summary>
        /// Add exception handling
        /// </summary>
        /// <param name="application">Builder for configuring an application's request pipeline</param>
        public static void UsePMSExceptionHandler(this IApplicationBuilder application)
        {
            var pmsConfig = EngineContext.Current.Resolve<PMSConfig>();
            var webHostEnvironment = EngineContext.Current.Resolve<IWebHostEnvironment>();
            var useDetailedExceptionPage = pmsConfig.DisplayFullErrorStack || webHostEnvironment.IsDevelopment();
            if (useDetailedExceptionPage)
            {
                //get detailed exceptions for developing and testing purposes
                application.UseDeveloperExceptionPage();
            }
            else
            {
                //or use special exception handler
                application.UseExceptionHandler(DefaultRoutePath.ErrPage); //("/Error/Error");
            }

            //log errors
            application.UseExceptionHandler(handler =>
            {
                handler.Run(context =>
                {
                    var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;
                    if (exception == null)
                        return Task.CompletedTask;

                    try
                    {
                        //get current customer
                        var currentCustomer = EngineContext.Current.Resolve<IWorkContext>().CurrentCustomer;

                        //log error
                        EngineContext.Current.Resolve<ILogger>().Error(exception.Message, exception, currentCustomer);
                    }
                    finally
                    {
                        //rethrow the exception to show the error page
                        ExceptionDispatchInfo.Throw(exception);
                    }

                    return Task.CompletedTask;
                });
            });
        }

        /// <summary>
        /// Adds a special handler that checks for responses with the 404 status code that do not have a body
        /// </summary>
        /// <param name="application">Builder for configuring an application's request pipeline</param>
        public static void UsePageNotFound(this IApplicationBuilder application)
        {
            application.UseStatusCodePages(async context =>
            {
                //handle 404 Not Found
                if (context.HttpContext.Response.StatusCode == StatusCodes.Status404NotFound)
                {
                    var webHelper = EngineContext.Current.Resolve<IWebHelper>();
                    if (!webHelper.IsStaticResource())
                    {
                        //get original path and query
                        var originalPath = context.HttpContext.Request.Path;
                        var originalQueryString = context.HttpContext.Request.QueryString;

                        var commonSettings = EngineContext.Current.Resolve<CommonSettings>();

                        if (commonSettings.Log404Errors)
                        {
                            var logger = EngineContext.Current.Resolve<ILogger>();
                            var workContext = EngineContext.Current.Resolve<IWorkContext>();
                            logger.Error($"Error 404. The requested page ({originalPath}) was not found",
                                customer: workContext.CurrentCustomer);
                        }

                        try
                        {
                            //get new path
                            var pageNotFoundPath = DefaultRoutePath.PageNotFound404; //"/page-not-found";
                            //re-execute request with new path
                            context.HttpContext.Response.Redirect(context.HttpContext.Request.PathBase + pageNotFoundPath);
                        }
                        finally
                        {
                            //return original path to request
                            context.HttpContext.Request.QueryString = originalQueryString;
                            context.HttpContext.Request.Path = originalPath;
                        }
                    }

                    await Task.CompletedTask;
                }
            });
        }

        /// <summary>
        /// Adds a special handler that checks for responses with the 400 status code (bad request)
        /// </summary>
        /// <param name="application">Builder for configuring an application's request pipeline</param>
        public static void UseBadRequestResult(this IApplicationBuilder application)
        {
            application.UseStatusCodePages(context =>
            {
                //handle 400 (Bad request)
                if (context.HttpContext.Response.StatusCode == StatusCodes.Status400BadRequest)
                {
                    var logger = EngineContext.Current.Resolve<ILogger>();
                    var workContext = EngineContext.Current.Resolve<IWorkContext>();
                    logger.Error("Error 400. Bad request", null, customer: workContext.CurrentCustomer);
                }

                return Task.CompletedTask;
            });
        }

        /// <summary>
        /// Configure middleware for dynamically compressing HTTP responses
        /// See: CommonSettings.UseResponseCompression
        /// </summary>
        /// <param name="application">Builder for configuring an application's request pipeline</param>
        public static void UsePMSResponseCompression(this IApplicationBuilder application)
        {
            //whether to use compression (gzip by default)
            if (EngineContext.Current.Resolve<CommonSettings>().UseResponseCompression)
                application.UseResponseCompression();
        }

        /// <summary>
        /// Configure static file serving
        /// </summary>
        /// <param name="application">Builder for configuring an application's request pipeline</param>
        public static void UsePMSStaticFiles(this IApplicationBuilder application)
        {
            static void staticFileResponse(StaticFileResponseContext context)
            {
                var commonSettings = EngineContext.Current.Resolve<CommonSettings>();
                if (!string.IsNullOrEmpty(commonSettings.StaticFilesCacheControl))
                    context.Context.Response.Headers.Append(HeaderNames.CacheControl, commonSettings.StaticFilesCacheControl);
            }

            var fileProvider = EngineContext.Current.Resolve<IPMSFileProvider>();

            //common static files
            application.UseStaticFiles(new StaticFileOptions { 
                OnPrepareResponse = staticFileResponse,
                //For statice files like "*.yml"
                //https://stackoverflow.com/questions/48972520/asp-net-core-2-0-web-application-static-files-gives-404
                ServeUnknownFileTypes = true,
            });

            //plugins static files
            var staticFileOptions = new StaticFileOptions
            {
                FileProvider = new PhysicalFileProvider(fileProvider.MapPath(@"Plugins")),
                RequestPath = new PathString("/Plugins"),
                OnPrepareResponse = staticFileResponse
            };

            var securitySettings = EngineContext.Current.Resolve<SecuritySettings>();
            if (!string.IsNullOrEmpty(securitySettings.PluginStaticFileExtensionsBlacklist))
            {
                var fileExtensionContentTypeProvider = new FileExtensionContentTypeProvider();

                foreach (var ext in securitySettings.PluginStaticFileExtensionsBlacklist
                    .Split(';', ',')
                    .Select(e => e.Trim().ToLower())
                    .Select(e => $"{(e.StartsWith(".") ? string.Empty : ".")}{e}")
                    .Where(fileExtensionContentTypeProvider.Mappings.ContainsKey))
                {
                    fileExtensionContentTypeProvider.Mappings.Remove(ext);
                }

                staticFileOptions.ContentTypeProvider = fileExtensionContentTypeProvider;
            }

            application.UseStaticFiles(staticFileOptions);
            #region 以下方法可以参考 但根据实际需要不使用此静态资源
            //add support for backups
            //var provider = new FileExtensionContentTypeProvider
            //{
            //    Mappings = { [".bak"] = MimeTypes.ApplicationOctetStream }
            //};

            //application.UseStaticFiles(new StaticFileOptions
            //{
            //    FileProvider = new PhysicalFileProvider(fileProvider.GetAbsolutePath(NopCommonDefaults.DbBackupsPath)),
            //    RequestPath = new PathString("/db_backups"),
            //    ContentTypeProvider = provider
            //});

            ////add support for webmanifest files
            //provider.Mappings[".webmanifest"] = MimeTypes.ApplicationManifestJson;

            //application.UseStaticFiles(new StaticFileOptions
            //{
            //    FileProvider = new PhysicalFileProvider(fileProvider.GetAbsolutePath("icons")),
            //    RequestPath = "/icons",
            //    ContentTypeProvider = provider
            //});
            #endregion
        }

        /// <summary>
        /// Configure middleware checking whether requested page is keep alive page
        /// </summary>
        /// <param name="application">Builder for configuring an application's request pipeline</param>
        public static void UseKeepAlive(this IApplicationBuilder application)
        {
            application.UseMiddleware<KeepAliveMiddleware>();
        }

        /// <summary>
        /// Adds the authentication middleware, which enables authentication capabilities.
        /// </summary>
        /// <param name="application">Builder for configuring an application's request pipeline</param>
        public static void UsePMSAuthentication(this IApplicationBuilder application)
        {
            //https://stackoverflow.com/questions/48836688/what-exactly-is-useauthentication-for
            application.UseMiddleware<AuthenticationMiddleware>();
        }

        /// <summary>
        /// Configure the request localization feature
        /// </summary>
        /// <param name="application">Builder for configuring an application's request pipeline</param>
        public static void UsePMSRequestLocalization(this IApplicationBuilder application)
        {
            application.UseRequestLocalization(options =>
            {
                //prepare supported cultures
                var langs = EngineContext.Current.Resolve<ILanguageService>().GetAllLanguages();
                var cultures = langs.OrderBy(language => language.DisplayOrder)
                    .Select(language => new CultureInfo(language.CultureCode)).ToList();
                options.SupportedCultures = cultures;
                try
                {
                    var currentStore = EngineContext.Current.Resolve<IStoreContext>().CurrentStore;
                    var curStoreLang = langs.FirstOrDefault(l => l.Id == currentStore.DefaultLanguageId);
                    options.DefaultRequestCulture = curStoreLang == null
                                                  ? throw new Exception("CurrentStore Language Not Matched")
                                                  : new RequestCulture(new CultureInfo(curStoreLang.CultureCode));
                } 
                catch
                {  //CurrentStore Not Load Exception
                   //CurrentStore Language Not Matched Exception
                    options.DefaultRequestCulture = new RequestCulture(cultures.FirstOrDefault());
                }
            });
        }

        /// <summary>
        /// Set current culture info
        /// </summary>
        /// <param name="application">Builder for configuring an application's request pipeline</param>
        public static void UseCulture(this IApplicationBuilder application)
        {
            application.UseMiddleware<CultureMiddleware>();
        }

        /// <summary>
        /// Configure Endpoints routing
        /// </summary>
        /// <param name="application">Builder for configuring an application's request pipeline</param>
        public static void UsePMSEndpoints(this IApplicationBuilder application)
        {
            //Add the EndpointRoutingMiddleware
            application.UseRouting();
            //Execute the endpoint selected by the routing middleware
            application.UseEndpoints(endpoints =>
            {
                //register all routes
                EngineContext.Current.Resolve<IRoutePublisher>().RegisterRoutes(endpoints);
            });
        }

        /// <summary>
        /// Configure WebMarkupMin
        /// </summary>
        /// <param name="application">Builder for configuring an application's request pipeline</param>
        public static void UsePMSWebMarkupMin(this IApplicationBuilder application)
        {
            application.UseWebMarkupMin();
        }
    }

}
